显示英雄列表

首先我们的目的是在画面上显示一个简单的英雄列表,效果如下:

英雄列表

第一步 创建英雄类

在util库下创建文件hero-list.service.model.ts,存放英雄的定义:

1
2
3
4
5
6
7
export interface Hero {
  heroId: string;
  name: string;
  gender: boolean;
  age: number;
  job: string;
}

然后在utils/src/index.ts中将其导出:

1
2
export * from './lib/utils.module';
export * from './lib/hero-list.service.model';

第二步 在Reducer中修改状态

reducer里定义了画面中将会出现的状态,由于我们只想显示一个简单的英雄列表,所以状态中只有一个list变量
hero-list-store.reducer.ts:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import {
  HeroListStoreAction,
  HeroListStoreActionTypes
} from './hero-list-store.actions';
import {
  Hero
} from '@sample-app/utils';

export const HEROLISTSTORE_FEATURE_KEY = 'heroListStore';

export interface HeroListStoreState {
  list: Hero[];     //想在画面上显示的数据的类型改为刚刚创建的Hero
}

export interface HeroListStorePartialState {
  readonly [HEROLISTSTORE_FEATURE_KEY]: HeroListStoreState;
}

//这是默认状态下的state定义
export const initialState: HeroListStoreState = {
  list: []
};

//state和action都是外部传入的
//state初始就是上面的默认定义
//action就是将要执行的动作类型
export function reducer(
  state: HeroListStoreState = initialState,
  action: HeroListStoreAction
): HeroListStoreState {
  switch (action.type) {
    //这个动作就是数据加载完成后的动作
    case HeroListStoreActionTypes.HeroListStoreLoaded: {
      //将state中其他值不变,但是改变list的值为取得到的数据
      state = {
        ...state,
        list: action.payload
      };
      break;
    }
  }
  return state;
}

第三步 修改selector中的查询

selector.ts中会存放一些画面上将会执行到的查询
hero-list-store.selectors.ts:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import { createFeatureSelector, createSelector } from '@ngrx/store';
import {
  HEROLISTSTORE_FEATURE_KEY,
  HeroListStoreState
} from './hero-list-store.reducer';

// Lookup the 'HeroListStore' feature state managed by NgRx
const getHeroListStoreState = createFeatureSelector<HeroListStoreState>(
  HEROLISTSTORE_FEATURE_KEY
);

//这个查询将会直接返回所有英雄列表
const getAllHeroListStore = createSelector(
  getHeroListStoreState,
  (state: HeroListStoreState) => state.list
);

export const heroListStoreQuery = {
  getAllHeroListStore
};

第三步 修改facade中的变量

facade会存放一些查询执行后的结果,还有一些改变状态的方法等等

现在还并没有做过多的修改,只是将之前删掉的一些变量在这边也一同删去
hero-list-store.facade.ts:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import { Injectable } from '@angular/core';

import { select, Store } from '@ngrx/store';

import { HeroListStorePartialState } from './hero-list-store.reducer';
import { heroListStoreQuery } from './hero-list-store.selectors';
import { LoadHeroListStore } from './hero-list-store.actions';

@Injectable()
export class HeroListStoreFacade {
  allHeroListStore$ = this.store.pipe(
    select(heroListStoreQuery.getAllHeroListStore)
  );

  constructor(private store: Store<HeroListStorePartialState>) {}

  loadAll() {
    this.store.dispatch(new LoadHeroListStore());
  }
}

第四步 修改action

action会定义在画面中所有可能执行到的动作
通常,如果在selector中新加了查询,那么这边一般也要添加一个动作

现在也没有做什么修改,只是将HeroListStoreLoaded中的类型改为了Hero
hero-list-store.actions.ts:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import { Action } from '@ngrx/store';
import { Hero } from '@sample-app/utils';

export enum HeroListStoreActionTypes {
  LoadHeroListStore = '[HeroListStore] Load HeroListStore',
  HeroListStoreLoaded = '[HeroListStore] HeroListStore Loaded',
  HeroListStoreLoadError = '[HeroListStore] HeroListStore Load Error'
}

export class LoadHeroListStore implements Action {
  readonly type = HeroListStoreActionTypes.LoadHeroListStore;
}

export class HeroListStoreLoadError implements Action {
  readonly type = HeroListStoreActionTypes.HeroListStoreLoadError;
  constructor(public payload: any) {}
}

export class HeroListStoreLoaded implements Action {
  readonly type = HeroListStoreActionTypes.HeroListStoreLoaded;
  constructor(public payload: Hero[]) {}
}

export type HeroListStoreAction =
  | LoadHeroListStore
  | HeroListStoreLoaded
  | HeroListStoreLoadError;

export const fromHeroListStoreActions = {
  LoadHeroListStore,
  HeroListStoreLoaded,
  HeroListStoreLoadError
};

现在,store中需要进行的一些操作就完成了,接下来就要修改画面了

第五步 创建table结构

在libs/content/hero-lib/src/lib/hero-list下创建hero-list.header.ts类,存放表的定义
ngx-datatable会用到这些信息:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import { TableColumn } from '@swimlane/ngx-datatable';

export const formatter = {
  gender: {
    transform: val => (val ? '男' : '女')
  },
};

export const columns: TableColumn[] = [
  {
    prop: 'heroId',
    name: '用户ID'
  },
  {
    prop: 'name',
    name: '用户名'
  },
  {
    prop: 'gender',
    name: '性别',
    pipe: formatter.gender
  },
  {
    prop: 'age',
    name: '年龄'
  },
  {
    prop: 'job',
    name: '职业'
  }
];

这里用到了@swimlane/ngx-datatable包,在npm中将其安装进来

1
npm i @swimlane/ngx-datatable

第六步 将要用到的控件包导入到module中

修改content-hero-lib.module.ts,将控件包导入进来:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { HeroListComponent } from './hero-list/hero-list.component';
import { NgxDatatableModule } from '@swimlane/ngx-datatable';

@NgModule({
  imports: [
    NgxDatatableModule,
    CommonModule,

    RouterModule.forChild([
      {path: '', pathMatch: 'full', component: HeroListComponent}
    ])
  ],
  declarations: [HeroListComponent]
})
export class ContentHeroLibModule {}

第六步 修改画面

hero-list.component.html:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<ngx-datatable
  #datatable
  [columns]="columns"
  [rows]="rows$ | async"
  class="material"
  [limit]="10"
  [headerHeight]="50"
  [footerHeight]="50"
  [rowHeight]="60"
  [scrollbarH]="true"
  [scrollbarV]="false"
  [selectionType]="false"
>
  <!-- Template Column -->
  <ngx-datatable-column
    *ngFor="let col of columns"
    [width]="col.width"
    [name]="col.name"
    [prop]="col.prop"
    [pipe]="col.pipe"
    [cellClass]="col.cellClass"
  >
  </ngx-datatable-column>
</ngx-datatable>

hero-list.component.ts:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { Hero } from '@sample-app/utils';
import { HeroListStoreFacade } from '@sample-app/store/hero-store';
import { columns } from './hero-list.header';

@Component({
  selector: 'chs-hero-list-hero-list',
  templateUrl: './hero-list.component.html',
  styleUrls: ['./hero-list.component.css']
})
export class HeroListComponent implements OnInit {
  columns = columns;

  rows$: Observable<Hero[]>;

  constructor(public authUsers: HeroListStoreFacade) {
    this.rows$ = this.authUsers.allHeroListStore$;
   }

  ngOnInit() {
  }

}

第七步 修改错误

运行之后,发现画面为空,chrome中按F12打开控制台发现以下错误:

错误

需要修改一些东西:

在apps/client/src/app/app.module.ts中,将一些包导入进来:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { NxModule } from '@nrwl/nx'; //导入的包
import { StoreModule } from '@ngrx/store'; //导入的包
import { EffectsModule } from '@ngrx/effects'; //导入的包

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    AppRoutingModule,
    NxModule.forRoot(), //导入的包
    StoreModule.forRoot({}), //导入的包
    EffectsModule.forRoot([]) //导入的包
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {}

这个时候会发现@nrwl/nx不存在,在package.json中将其添加,然后执行npm install安装进项目中

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
  ...
  "dependencies": {
    "@angular/animations": "^8.2.0",
    "@angular/common": "^8.2.0",
    "@angular/compiler": "^8.2.0",
    "@angular/core": "^8.2.0",
    "@angular/forms": "^8.2.0",
    "@angular/platform-browser": "^8.2.0",
    "@angular/platform-browser-dynamic": "^8.2.0",
    "@angular/router": "^8.2.0",
    "@ngrx/effects": "8.5.0",
    "@ngrx/entity": "8.5.0",
    "@ngrx/router-store": "8.5.0",
    "@ngrx/store": "8.5.0",
    "@nrwl/nx": "7.1.1",    //添加@nrwl/nx
    "@swimlane/ngx-datatable": "^16.0.2",
    "core-js": "^2.5.4",
    "rxjs": "~6.4.0",
    "zone.js": "^0.9.1"
  },
  ...

然后是libs/content/hero-lib/src/lib/content-hero-lib.module.ts,需要将store的module导入进来
还要将facade进行依赖注入:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule, Router } from '@angular/router';
import { HeroListComponent } from './hero-list/hero-list.component';
import { NgxDatatableModule } from '@swimlane/ngx-datatable';
import { StoreHeroStoreModule, HeroListStoreFacade } from '@sample-app/store/hero-store';

@NgModule({
  imports: [
    NgxDatatableModule,
    StoreHeroStoreModule,   //导入store的module
    CommonModule,

    RouterModule.forChild([
      {path: '', pathMatch: 'full', component: HeroListComponent}
    ])
  ],
  declarations: [HeroListComponent]
})
export class ContentHeroLibModule {
  //将Router和HeroListStoreFacade进行依赖注入,在这个module初始化的时候就将数据载入
  constructor(private router: Router, private authUsers: HeroListStoreFacade) {
    this.router.events
      .subscribe(comp => {
        this.authUsers.loadAll();   //载入数据
      });
  }
}

最后是libs/store/hero-store/src/lib/+state/hero-list-store.effects.ts
这里要将DataPersistence类从@nrwl/angular改成从@nrwl/nx导入:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import { Injectable } from '@angular/core';
import { Effect, Actions } from '@ngrx/effects';
import { DataPersistence } from '@nrwl/nx';     //修改导入的包

import { HeroListStorePartialState } from './hero-list-store.reducer';
import {
  LoadHeroListStore,
  HeroListStoreLoaded,
  HeroListStoreLoadError,
  HeroListStoreActionTypes
} from './hero-list-store.actions';

@Injectable()
export class HeroListStoreEffects {
  @Effect() loadHeroListStore$ = this.dataPersistence.fetch(
    HeroListStoreActionTypes.LoadHeroListStore,
    {
      run: (action: LoadHeroListStore, state: HeroListStorePartialState) => {
        // Your custom REST 'load' logic goes here. For now just return an empty list...
        return new HeroListStoreLoaded([]);
      },

      onError: (action: LoadHeroListStore, error) => {
        console.error('Error', error);
        return new HeroListStoreLoadError(error);
      }
    }
  );

  constructor(
    private actions$: Actions,
    private dataPersistence: DataPersistence<HeroListStorePartialState>
  ) {}
}

这些错误应该是因为版本问题导致的,参考的日本项目是7.*的版本,但是新创建的项目都是8.*

添加假数据

然后运行画面:

画面

画面已经显示出来了,但是还没有数据
因为数据是从effect中加载的,在effect中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
  @Effect() loadHeroListStore$ = this.dataPersistence.fetch(
    HeroListStoreActionTypes.LoadHeroListStore,
    {
      run: (action: LoadHeroListStore, state: HeroListStorePartialState) => {
        // Your custom REST 'load' logic goes here. For now just return an empty list...
        //这里返回了一个空的数据,所以画面上没有数据显示
        return new HeroListStoreLoaded([]);
      },

      onError: (action: LoadHeroListStore, error) => {
        console.error('Error', error);
        return new HeroListStoreLoadError(error);
      }
    }
  );

正式项目中,这里会从api取值,然后再从mock中返回假的值,这里简单处理就直接给常量
修改libs/src/lib/hero-list.service.model.ts,在这里设置一个固定的数据:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
export interface Hero {
  heroId: string;
  name: string;
  gender: boolean;
  age: number;
  job: string;
}

export class HeroObject implements Hero {
  heroId: string;
  name: string;
  gender: boolean;
  age: number;
  job: string;
}

export let HERO_LIST: HeroObject[] = [
  { heroId: 'hero1', name: 'JAMES', gender: true, age: 1 , job: `魔法师`},
  { heroId: 'hero2', name: 'JOHN', gender: true, age: 12 , job: `战士`},
  { heroId: 'hero3', name: 'ROBERT', gender: true, age: 13 , job: `圣骑士`},
  { heroId: 'hero4', name: 'MICHAEL', gender: true, age: 14 , job: `牧师`},
  { heroId: 'hero5', name: 'MARY', gender: false, age: 51 , job: `萨满`},
  { heroId: 'hero6', name: 'PATRICIA', gender: false, age: 16 , job: `盗贼`},
  { heroId: 'hero7', name: 'LINDA', gender: false, age: 17 , job: `德鲁伊`},
  { heroId: 'hero8', name: 'BARBARA', gender: false, age: 81 , job: `术士`},
  { heroId: 'hero9', name: 'ELIZABETH', gender: false, age: 19 , job: `猎人`},
  { heroId: 'hero0', name: 'JENNIFER', gender: false, age: 20, job: `牧师` }
];

然后在effect中直接将常量返回:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import { Injectable } from '@angular/core';
import { Effect, Actions } from '@ngrx/effects';
import { DataPersistence } from '@nrwl/nx';

import { HeroListStorePartialState } from './hero-list-store.reducer';
import {
  LoadHeroListStore,
  HeroListStoreLoaded,
  HeroListStoreLoadError,
  HeroListStoreActionTypes
} from './hero-list-store.actions';
import { HERO_LIST } from '@sample-app/utils';
import { map } from 'rxjs/operators';
import { of } from 'rxjs';

@Injectable()
export class HeroListStoreEffects {
  @Effect() loadHeroListStore$ = this.dataPersistence.fetch(
    HeroListStoreActionTypes.LoadHeroListStore,
    {
      run: (action: LoadHeroListStore, state: HeroListStorePartialState) => {
        return  of(HERO_LIST)
        .pipe(map(users => new HeroListStoreLoaded(users)));    //直接返回常量
      },

      onError: (action: LoadHeroListStore, error) => {
        console.error('Error', error);
        return new HeroListStoreLoadError(error);
      }
    }
  );

  constructor(
    private actions$: Actions,
    private dataPersistence: DataPersistence<HeroListStorePartialState>
  ) {}
}

运行项目:

画面

现在画面上就有数据了